<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>网页爬虫工具</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#0FC6C2',
            neutral: '#F2F3F5',
            dark: '#1D2129',
          },
          fontFamily: {
            inter: ['Inter', 'system-ui', 'sans-serif'],
          },
        },
      }
    }
  </script>
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .bg-gradient-primary {
        background: linear-gradient(135deg, #165DFF 0%, #0FC6C2 100%);
      }
      .text-shadow {
        text-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .transition-all-300 {
        transition: all 300ms ease-in-out;
      }
    }
  </style>
</head>
<body class="font-inter bg-gray-50 text-dark min-h-screen flex flex-col">
  <!-- 顶部导航 -->
  <header class="bg-white shadow-sm sticky top-0 z-50">
    <div class="container mx-auto px-4 py-4 flex justify-between items-center">
      <div class="flex items-center space-x-2">
        <i class="fa fa-spider text-primary text-2xl"></i>
        <h1 class="text-xl md:text-2xl font-bold bg-clip-text text-transparent bg-gradient-primary">WebScraper</h1>
      </div>
      <nav class="hidden md:flex items-center space-x-6">
        <a href="#" class="text-gray-600 hover:text-primary transition-all-300">首页</a>
        <a href="#" class="text-gray-600 hover:text-primary transition-all-300">文档</a>
        <a href="#" class="text-gray-600 hover:text-primary transition-all-300">示例</a>
        <a href="#" class="text-gray-600 hover:text-primary transition-all-300">关于</a>
      </nav>
      <button class="md:hidden text-gray-600">
        <i class="fa fa-bars text-xl"></i>
      </button>
    </div>
  </header>

  <!-- 主内容区 -->
  <main class="flex-grow container mx-auto px-4 py-8">
    <div class="max-w-5xl mx-auto">
      <!-- 介绍卡片 -->
      <div class="bg-white rounded-xl shadow-md p-6 mb-8 transform hover:shadow-lg transition-all-300">
        <h2 class="text-2xl font-bold mb-3 text-dark">网页爬虫工具</h2>
        <p class="text-gray-600 mb-4">
          这是一个功能强大的网页爬虫工具，支持CSV等多种格式导出。
          输入东方财富网等股票行情URL，设置保存选项，即可获取并保存数据。
        </p>
        <div class="flex flex-wrap gap-4">
          <div class="flex items-center">
            <i class="fa fa-check-circle text-green-500 mr-2"></i>
            <span class="text-gray-600">支持CSV/JSON等格式</span>
          </div>
          <div class="flex items-center">
            <i class="fa fa-check-circle text-green-500 mr-2"></i>
            <span class="text-gray-600">自动翻页爬取多页数据</span>
          </div>
          <div class="flex items-center">
            <i class="fa fa-check-circle text-green-500 mr-2"></i>
            <span class="text-gray-600">实时显示爬取进度</span>
          </div>
        </div>
      </div>

      <!-- 爬虫表单 -->
      <div class="bg-white rounded-xl shadow-md p-6 mb-8">
        <h3 class="text-xl font-bold mb-4 text-dark">爬取设置</h3>
        <form id="crawlerForm" class="space-y-4">
          <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div>
              <label for="url" class="block text-sm font-medium text-gray-700 mb-1">URL <span class="text-red-500">*</span></label>
              <div class="relative">
                <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                  <i class="fa fa-globe text-gray-400"></i>
                </div>
                <input type="url" id="url" name="url" required
                  class="pl-10 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-primary focus:border-primary sm:text-sm border p-2.5"
                  placeholder="https://example.com" value="https://quote.eastmoney.com/center/gridlist.html#hs_a_board">
              </div>
            </div>
            <div>
              <label for="fileName" class="block text-sm font-medium text-gray-700 mb-1">保存文件名 <span class="text-red-500">*</span></label>
              <div class="relative">
                <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                  <i class="fa fa-file-text-o text-gray-400"></i>
                </div>
                <input type="text" id="fileName" name="fileName" required
                  class="pl-10 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-primary focus:border-primary sm:text-sm border p-2.5"
                  placeholder="my-data" value="stock_data">
              </div>
            </div>
            <div>
              <label for="fileType" class="block text-sm font-medium text-gray-700 mb-1">文件类型 <span class="text-red-500">*</span></label>
              <div class="relative">
                <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                  <i class="fa fa-file-code-o text-gray-400"></i>
                </div>
                <select id="fileType" name="fileType" required
                  class="pl-10 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-primary focus:border-primary sm:text-sm border p-2.5 appearance-none bg-white">
                  <option value="csv">CSV 文件 (.csv)</option>
                  <option value="json">JSON 文件 (.json)</option>
                  <option value="txt">文本文件 (.txt)</option>
                </select>
                <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
                  <i class="fa fa-chevron-down text-gray-400"></i>
                </div>
              </div>
            </div>
            <div>
              <label for="encoding" class="block text-sm font-medium text-gray-700 mb-1">文件编码 <span class="text-red-500">*</span></label>
              <div class="relative">
                <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                  <i class="fa fa-language text-gray-400"></i>
                </div>
                <select id="encoding" name="encoding" required
                  class="pl-10 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-primary focus:border-primary sm:text-sm border p-2.5 appearance-none bg-white">
                  <option value="utf-8">UTF-8</option>
                  <option value="gbk">GBK</option>
                </select>
                <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
                  <i class="fa fa-chevron-down text-gray-400"></i>
                </div>
              </div>
            </div>
          </div>

          <div class="pt-2">
            <button type="submit" id="submitBtn"
              class="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-primary hover:opacity-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all-300 shadow-md hover:shadow-lg">
              <span class="absolute inset-y-0 left-0 flex items-center pl-3">
                <i class="fa fa-crawl fa-spider group-hover:animate-bounce transition-all-300"></i>
              </span>
              开始爬取
            </button>
          </div>
        </form>
      </div>

      <!-- 爬虫结果 -->
      <div class="bg-white rounded-xl shadow-md p-6 mb-8">
        <div class="flex justify-between items-center mb-4">
          <h3 class="text-xl font-bold text-dark">爬取结果</h3>
          <div class="flex space-x-2">
            <button id="clearBtn" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm transition-all-300">
              <i class="fa fa-trash-o mr-1"></i> 清空
            </button>
            <button id="saveBtn" class="px-3 py-1.5 bg-primary hover:bg-primary/90 text-white rounded-md text-sm transition-all-300" disabled>
              <i class="fa fa-download mr-1"></i> 保存
            </button>
          </div>
        </div>

        <div class="relative h-96">
          <div id="loadingIndicator" class="absolute inset-0 flex items-center justify-center bg-white bg-opacity-80 z-10 hidden">
            <div class="flex flex-col items-center">
              <div class="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
              <p class="mt-3 text-gray-600">正在爬取数据，请稍候...</p>
            </div>
          </div>

          <div id="errorMessage" class="absolute inset-0 flex items-center justify-center bg-white bg-opacity-80 z-10 hidden">
            <div class="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
              <i class="fa fa-exclamation-circle mr-2"></i>
              <span id="errorText">爬取失败，请检查URL是否正确</span>
            </div>
          </div>

          <textarea id="resultTextarea" class="w-full h-full p-4 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary resize-none text-sm font-mono" placeholder="爬取的内容将显示在这里..." disabled></textarea>
        </div>

        <div class="mt-3 text-sm text-gray-500">
          <span id="statusText">准备就绪</span>
          <span id="wordCount" class="ml-4 hidden">
            <i class="fa fa-file-text-o mr-1"></i> <span>0</span> 个字符
          </span>
          <span id="pageCount" class="ml-4 hidden">
            <i class="fa fa-pagelines mr-1"></i> <span>0</span> 页
          </span>
        </div>
      </div>

      <!-- 爬取进度 -->
      <div id="progressContainer" class="bg-white rounded-xl shadow-md p-6 mb-8 hidden">
        <h3 class="text-xl font-bold mb-4 text-dark">爬取进度</h3>
        <div class="relative pt-1">
          <div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-gray-200">
            <div id="progressBar" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-gradient-primary transition-all duration-300 ease-in-out" style="width: 0%"></div>
          </div>
        </div>
        <div id="progressLog" class="max-h-60 overflow-y-auto bg-gray-50 p-3 rounded-lg text-sm font-mono">
          <!-- 爬取日志将在这里显示 -->
        </div>
      </div>
    </div>
  </main>

  <!-- 页脚 -->
  <footer class="bg-white border-t border-gray-200">
    <div class="container mx-auto px-4 py-6">
      <div class="flex flex-col md:flex-row justify-between items-center">
        <div class="mb-4 md:mb-0">
          <p class="text-gray-600 text-sm">© 2025 WebScraper. 保留所有权利。</p>
        </div>
        <div class="flex space-x-4">
          <a href="#" class="text-gray-400 hover:text-primary transition-all-300">
            <i class="fa fa-github text-xl"></i>
          </a>
          <a href="#" class="text-gray-400 hover:text-primary transition-all-300">
            <i class="fa fa-twitter text-xl"></i>
          </a>
          <a href="#" class="text-gray-400 hover:text-primary transition-all-300">
            <i class="fa fa-linkedin text-xl"></i>
          </a>
        </div>
      </div>
    </div>
  </footer>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const form = document.getElementById('crawlerForm');
      const urlInput = document.getElementById('url');
      const fileNameInput = document.getElementById('fileName');
      const fileTypeSelect = document.getElementById('fileType');
      const encodingSelect = document.getElementById('encoding');
      const resultTextarea = document.getElementById('resultTextarea');
      const submitBtn = document.getElementById('submitBtn');
      const clearBtn = document.getElementById('clearBtn');
      const saveBtn = document.getElementById('saveBtn');
      const loadingIndicator = document.getElementById('loadingIndicator');
      const errorMessage = document.getElementById('errorMessage');
      const errorText = document.getElementById('errorText');
      const statusText = document.getElementById('statusText');
      const wordCount = document.getElementById('wordCount');
      const pageCount = document.getElementById('pageCount');
      const progressContainer = document.getElementById('progressContainer');
      const progressBar = document.getElementById('progressBar');
      const progressLog = document.getElementById('progressLog');

      // 后端API地址
      const API_URL = 'http://127.0.0.1:8080/spider';

      let totalPages = 0;
      let currentProgress = 0;

      // 清空按钮事件
      clearBtn.addEventListener('click', function() {
        resultTextarea.value = '';
        updateStatus('内容已清空', false);
        saveBtn.disabled = true;
        wordCount.classList.add('hidden');
        pageCount.classList.add('hidden');
        progressContainer.classList.add('hidden');
      });

      // 保存按钮事件
      saveBtn.addEventListener('click', function() {
        const content = resultTextarea.value;
        if (!content.trim()) {
          showError('没有可保存的内容');
          return;
        }

        const fileName = fileNameInput.value.trim() || 'web-scraped-data';
        const fileType = fileTypeSelect.value;
        const encoding = encodingSelect.value;
        const fullFileName = `${fileName}.${fileType}`;

        // 创建Blob对象
        let contentType = 'text/plain';
        if (fileType === 'json') {
          contentType = 'application/json';
        } else if (fileType === 'csv') {
          contentType = 'text/csv';
        }

        const blob = new Blob([content], { type: `${contentType};charset=${encoding}` });

        // 创建下载链接
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = fullFileName;

        // 模拟点击下载
        document.body.appendChild(a);
        a.click();

        // 清理
        setTimeout(() => {
          document.body.removeChild(a);
          URL.revokeObjectURL(url);
        }, 100);

        updateStatus(`已成功保存为 ${fullFileName}`, false);
      });

      // 表单提交事件
      form.addEventListener('submit', function(e) {
        e.preventDefault();

        const url = urlInput.value.trim();
        if (!url) {
          showError('请输入有效的URL');
          return;
        }

        // 重置状态
        resultTextarea.value = '';
        saveBtn.disabled = true;
        wordCount.classList.add('hidden');
        pageCount.classList.add('hidden');
        totalPages = 0;
        currentProgress = 0;

        // 显示进度容器
        progressContainer.classList.remove('hidden');
        progressBar.style.width = '0%';
        progressLog.innerHTML = '';
        addProgressLog(`开始爬取: ${url}`);

        // 显示加载状态
        loadingIndicator.classList.remove('hidden');
        updateStatus(`正在爬取 ${url}...`, true);

        // 调用后端API
        crawlWebsite(url)
          .then(response => {
            const { success, data, totalPages, message } = response;
            if (!success) {
              throw new Error(message);
            }

            resultTextarea.value = data;
            saveBtn.disabled = false;
            updateStatus(`爬取完成，共获取 ${data.length} 个字符，${totalPages} 页数据`, false);

            // 显示统计信息
            wordCount.querySelector('span').textContent = data.length;
            wordCount.classList.remove('hidden');

            pageCount.querySelector('span').textContent = totalPages;
            pageCount.classList.remove('hidden');

            addProgressLog(message);

          })
          .catch(error => {
            showError(error.message || '爬取失败，请重试');
          })
          .finally(() => {
            loadingIndicator.classList.add('hidden');
          });
      });

      // 更新状态文本
      function updateStatus(text, isLoading) {
        statusText.textContent = text;
        statusText.className = isLoading ? 'text-primary' : 'text-gray-500';
      }

      // 显示错误信息
      function showError(message) {
        errorText.textContent = message;
        errorMessage.classList.remove('hidden');

        setTimeout(() => {
          errorMessage.classList.add('hidden');
        }, 5000);

        updateStatus('爬取失败', false);
      }

      // 添加日志到进度面板
      function addProgressLog(message, isError = false) {
        const logEntry = document.createElement('div');
        logEntry.className = isError ? 'text-red-500 mb-1' : 'text-gray-700 mb-1';
        logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
        progressLog.appendChild(logEntry);
        progressLog.scrollTop = progressLog.scrollHeight;
      }

      // 更新进度条
      function updateProgress(percent) {
        progressBar.style.width = `${percent}%`;
      }

      // 调用后端爬虫API
      function crawlWebsite(url) {
        return new Promise((resolve, reject) => {
          const requestData = {
            url: url,
            filename: fileNameInput.value,
            fileType: fileTypeSelect.value,
            encoding: encodingSelect.value
          };

          axios.post(API_URL, requestData)
            .then(response => {
              resolve(response.data);
            })
            .catch(error => {
              console.error('API请求错误:', error);

              if (error.response) {
                // 服务器返回错误状态码
                addProgressLog(`API返回错误: ${error.response.status} ${error.response.data.message}`, true);
                reject(new Error(`API错误: ${error.response.data.message}`));
              } else if (error.request) {
                // 请求已发送，但没有收到响应
                addProgressLog(`无法连接到API服务器，请确保后端已启动`, true);
                reject(new Error('无法连接到API服务器，请确保后端已启动'));
              } else {
                // 设置请求时出错
                addProgressLog(`请求配置错误: ${error.message}`, true);
                reject(new Error(`请求配置错误: ${error.message}`));
              }
            });
        });
      }
    });
  </script>
<script>
// 双击返回上一级页面
document.addEventListener('dblclick', function() {
  // 添加双击反馈效果（可选）
  const feedback = document.createElement('div');
  feedback.style.cssText = `
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: rgba(0,0,0,0.7);
    color: white;
    padding: 10px 20px;
    border-radius: 4px;
    z-index: 9999;
    font-size: 16px;
    transition: opacity 0.3s;
  `;
  feedback.textContent = '返回上一级...';
  document.body.appendChild(feedback);

  // 300ms后执行返回操作，给用户视觉反馈
  setTimeout(() => {
    window.history.back();
  }, 300);

  // 自动移除反馈元素
  setTimeout(() => {
    document.body.removeChild(feedback);
  }, 1000);
});
</script>
</body>
</html>